class Gui {

  ProjectList projectList;
  Project currentProject;
  Environment currentModel;
  View currentView;
  
  Stylesheet styles;
  Toolbar projectsToolbar, modelsToolbar=null, viewsToolbar=null, controller=null; // these should be allowed to remain null if appropriate
  SliderList paramSliders;
  Slider speedSlider;
    
  boolean isPlaying, savingScreenshot = false;
  float desiredSpeed, realSpeed, dt;
  String errorMsg;
  float archiveSaveTime = -1000, screenshotSaveTime = -1000;

  
  
  Gui(ProjectList projectList, Stylesheet styles) {
    this.styles = styles;
    this.projectList = projectList;
    projectsToolbar = buildProjectsToolbar();
    selectProject(projectList.currentProject.shortname);
  }
  
  
  void selectProject(String key) {
    currentProject = (Project) projectList.get(key);
    projectList.currentProject = currentProject;
    if (projectsToolbar != null) {
      projectsToolbar.deselect();
      projectsToolbar.select(key);
    }
    if (currentProject.modelsToolbar == null && currentProject.size() > 1) currentProject.modelsToolbar = buildModelsToolbar(currentProject);
    modelsToolbar = currentProject.modelsToolbar;
    selectModel(currentProject.currentModel.shortname);
  }
  
  void selectModel(String key) {
    currentModel = (Environment) currentProject.get(key);
    currentProject.currentModel = currentModel;
    if (currentModel.awakened == false) {
      currentModel.wakeup();
      isPlaying = true;
    }
    if (modelsToolbar != null) {
      modelsToolbar.deselect();
      modelsToolbar.select(key);
    }
    if (currentModel.viewsToolbar == null) currentModel.viewsToolbar = buildViewsToolbar(currentModel);
    viewsToolbar = currentModel.viewsToolbar;
    if (currentModel.controller == null) currentModel.controller = buildController(currentModel);  
    controller = currentModel.controller;
    speedSlider = (Slider)controller.getElement("speed");
    if (currentModel.paramSliders == null) currentModel.paramSliders = buildParamSliders(currentModel);
    paramSliders = currentModel.paramSliders;
    selectView(currentModel.currentView.shortname);
  }
  
  void selectView(String key) {
    currentView = (View) currentModel.views.get(key);
    currentModel.currentView = currentView;
    if (currentView == null) currentView = new View();
    if (currentView.awakened == false) currentView.wakeup(this);
    viewsToolbar.deselect();
    viewsToolbar.select(key);
    if (currentView.toolbar == null || currentView.toolbar.hidden) {
      viewsToolbar.setTop(defaultViewsToolbarTop());
    } else {
      if (!viewsToolbar.hidden) viewsToolbar.setTop(defaultViewsToolbarTop() - styles.guiHeight - styles.guiMargin);
      currentView.toolbar.setTop(defaultViewsToolbarTop());
    }
  }
  
  
  
  void update() {
    boolean hit = false;
    String target = "";
    
    background(styles.bkgndColor);
    
    if (projectsToolbar != null) {
      hit = projectsToolbar.update();
      if (hit) selectProject(projectsToolbar.lastUpdated);
    }
    
    if (modelsToolbar != null) {
      hit = modelsToolbar.update();
      if (hit) selectModel(modelsToolbar.lastUpdated);
    }
  
    if (viewsToolbar != null) {
      hit = viewsToolbar.update();
      if (hit) selectView(viewsToolbar.lastUpdated);
    }
    
    pushStyle();
    stroke(255);
    strokeWeight(1.5);
    if (modelsToolbar != null) {
      line(0, modelsToolbar.bottom(), width, modelsToolbar.bottom());
    } else if (projectsToolbar != null) {
      line(0, projectsToolbar.bottom(), width, projectsToolbar.bottom());
    }
    if (viewsToolbar != null) line(0, viewsToolbar.top(), width, viewsToolbar.top());
    popStyle();
    
    displayTitleText();
    
    currentView.update(); // just gui controls and background stuff--not the main drawing. the View is repsonsible for handling the results of any clicks in itself
    
    hit = paramSliders.update();
    if (hit) {
      currentModel.updatePublicParams();
      currentModel.updatePrivateParams();
    }

    hit = controller.update();
    if (hit) {
      target = controller.lastUpdated;
      if (target.equals("REW")) {
        currentModel.rewind();
      } else if (target.equals("PLAY") || (target.equals("PAUSE"))) {
        isPlaying = !isPlaying;
      } else if (target.equals("SHOW\nARROWS") || target.equals("HIDE\nARROWS")) {
        currentModel.toggleArrows();
      } else if (target.equals("MORE\nDETAIL") || target.equals("LESS\nDETAIL")) {
        currentModel.toggleDetails();
      } else if (target.equals("SAVE\nDATA")) {
        currentModel.saveArchive();
        archiveSaveTime = millis();
      } else if (target.equals("SAVE\nIMAGE")) {
        savingScreenshot = true;
        beginRecord(PDF, currentModel.shortname + millis() +".pdf"); 
        screenshotSaveTime = millis();      
      } else if (target.equals("RESET\nPARAMS")) {
        currentModel.resetParams();
      }
    }
    
    if (isPlaying) {
      desiredSpeed = speedSlider.getVal();
      dt = desiredSpeed / max(20, frameRate);
      realSpeed = dt * frameRate;
      if (millis() > 1000) speedSlider.setVal(realSpeed); // except in the 1st second of runtime, when frameRate isn't accurate yet, relax desired speed toward what's achieveable (the gradual change of frameRate does the relaxing)
      speedSlider.writeOnRight(speedSlider.val2string(realSpeed) + " " + currentModel.time.units + "/sec (t = " + round(currentModel.time.current[0]) + " " + currentModel.time.units + ")");
      errorMsg = currentModel.integrate(dt); // *** the real action ***
      // might want to do something with that error message
    } else {
      speedSlider.writeOnRight("stopped (t = " + round(currentModel.time.current[0]) + " " + currentModel.time.units + ")");
    }
    
    if (currentModel.time.current[0] > currentModel.stopTime) {
      isPlaying = false;
      speedSlider.highlight = true;
      IconButton playBtn = (IconButton)controller.getElement("PAUSE");
      if (playBtn != null) playBtn.current = (playBtn.current+1) % playBtn.names.length;
    } else {
      speedSlider.highlight = false;
    }
    
    displayCredits();
    currentView.draw(); // *** the other real action ***
    
    if (savingScreenshot) endRecord();
    if (screenshotSaveTime + 1000 > millis()) writeHuge("image saved");
    if (archiveSaveTime + 1000 > millis()) writeHuge("archived"); // notification if an archive was saved recently
  }
  
  
  
  void offerMousePress() {
    boolean captured = currentView.offerMousePress();
    if (!captured && controller != null) captured = controller.offerMousePress();    
    if (!captured && paramSliders != null) captured = paramSliders.offerMousePress();
    if (!captured && viewsToolbar != null) captured = viewsToolbar.offerMousePress();
    if (!captured && modelsToolbar != null) captured = modelsToolbar.offerMousePress();
    if (!captured && projectsToolbar != null) captured = projectsToolbar.offerMousePress();    
  }
  
  
  // building toolbars -------------------------------------------------------------------------------
  
  
  Toolbar buildProjectsToolbar() {
    if (projectList.size() <= 1) return null; // no toolbar if only one entry
    Toolbar bar = new Toolbar(0, 0, width, styles);
    Iterator i = projectList.values().iterator();
    bar.addTextLabel("projects");
    while (i.hasNext()) {
      String name = ((Project) i.next()).shortname;
      bar.addTextButton(name);
    }   
    bar.addSpacer();
    return bar;
  }
  
  Toolbar buildModelsToolbar(Project p) {
    if (p.size() <= 1) return null; // no toolbar if only one entry
//    float x0 = projectsToolbar.unoccLeft;
//    float y0 = projectsToolbar.top();
    float x0 = 0;
    float y0 = 0;
    if (projectsToolbar != null) y0 = projectsToolbar.bottom();
    Toolbar bar = new Toolbar(x0, y0, width, styles);
    bar.addTextLabel("model cases");
    Iterator i = p.values().iterator();
    while (i.hasNext()) {
      String name = ((Environment) i.next()).shortname;
      bar.addTextButton(name);
    }   
    return bar; 
  }
  
  Toolbar buildController(Environment env) {
    float y0;
    if (modelsToolbar != null) {
      y0 = modelsToolbar.bottom() + 0.75*styles.guiMargin;
    } else if (projectsToolbar != null) {
      y0 = projectsToolbar.bottom() + 0.75*styles.guiMargin;
    } else {
      y0 = 0.75*styles.guiMargin;
    }
    Toolbar bar = new Toolbar(0, y0, width, styles);
    IconButton btn;
    bar.addIconButton("REW", "icons/rewind.png");
    btn = bar.addIconButton("PAUSE", "icons/pause.png");
    btn.addState("PLAY", "icons/play.png");
    speedSlider = bar.addSlider("speed", 4*styles.guiHeight, 0.2, 20);
    speedSlider.logScale = true;
    speedSlider.showValOnRight = false; // will do this manually, in order to display real speed instead of desired speed
    speedSlider.showNameOnLeft = false;
    bar.addSpacer();
    bar.addSpacer();
    bar.addIconButton("SAVE\nDATA", "icons/write.png");
//    bar.addIconButton("SAVE\nIMAGE", "icons/write.png"); // disabled for now because of text-drawing issues in the PDF library
    bar.addSpacer();
    bar.addIconButton("RESET\nPARAMS", "icons/rewind-circle.png");
    return bar;   
  }
  
  SliderList buildParamSliders(Environment env) {
    float y0 = controller.bottom() + countLines(env.name) * 1.1 * styles.largeFontSize + (countLines(env.commentary) + 1) * 1.1 * styles.normalFontSize;
    SliderList sl = new SliderList(width - 150, y0, 4*styles.guiHeight, styles);
    for (int i=0; i<env.params.length; i++) {
      FreeParam p = env.params[i];
      if (!p.hidden) {
        p.slider = sl.addSlider(p.name, p.min, p.max);
        p.slider.logScale = p.logScale;
        if (p.quantization > 0) p.slider.quantize(p.quantization);
        p.slider.highlight = p.highlight;
        p.slider.setVal(p.current);
      }
    }
    return sl;
  }

  float defaultViewsToolbarTop() {
    return height-styles.guiHeight;
  }
  
  Toolbar buildViewsToolbar(Environment env) {
    Toolbar bar = new Toolbar(0, defaultViewsToolbarTop(), width, styles);
    bar.addTextLabel("views");
    Iterator i = env.views.values().iterator();
    while (i.hasNext()) {
      String name = ((View) i.next()).shortname;
      bar.addTextButton(name);
    }  
    if (bar.elements.length <= 2) bar.hidden = true; // don't show the toolbar if it's just the label + one button 
    return bar;
  }


  // other display elements -------------------------------------------------------------------------
  

  void displayTitleText() {
    pushStyle();
    styles.setFont(styles.largeFontSize);
    float leading = 1.1 * styles.largeFontSize;
    textLeading(leading);
    textAlign(RIGHT);
    fill(styles.labelColor);
    float x0 = width - styles.guiMargin;
    float y0 = controller.y0 + 0.5*controller.height + 0.5*(textAscent()-textDescent());
    text(currentModel.name, x0, y0);
    y0 += countLines(currentModel.name) * leading;
    styles.setFont(styles.normalFontSize);
    leading = 1.1 * styles.normalFontSize;
    y0 += leading;
    textLeading(leading);
    text(currentModel.commentary, x0, y0);
  }
  
  void displayCredits() {
    String S = versionString + "\n" + creditsString;
    styles.setFont(styles.smallFontSize);
    fill(styles.labelColor);
    textAlign(RIGHT);
    text(S, width-textWidth("  "), height - textDescent() - 1);
  }
  
  
  void writeHuge(String S) {
    styles.setFont(72);
    textAlign(CENTER);
    fill(transparency(styles.labelColor,0.7));
    stroke(styles.labelColor);
    strokeWeight(1);
    text(S, width/2, height/2);
  }



  Axes availableViewSpace() {
    // an Axes that spans the onscreen space that Views can lay claim to,
    // with the square that spans the minimum dimension mapped onto -1..1, -1..1
    // call this in wakeup(), not constructors--the toolbars may not get built until a model is selected
    
    float x0 = styles.guiMargin;
    float y0 = controller.bottom() + styles.guiMargin; // how much space to leave for the top toolbars
    float x1 = paramSliders.left() - 80;
    float y1 = viewsToolbar.top();
    
    Axes ax = new Axes(x0, y0, x1-x0, y1-y0);
    ax.setYRange(-1, 1);
    float asp = abs((ax.right()-ax.left()) / (ax.bottom()-ax.top()));
    ax.setXRange(-asp, asp);
    return ax;
  }
  
}
